"""based on http://code.activestate.com/recipes/577784-line-based-side-by-side-diff/?in=lang-python

Generate line by line side by side diffs using http://code.google.com/p/google-diff-match-patch/

Modified to support older versions of python 2.4 (rather than 2.6)

Also adds on useful utility functions for getting html output.
"""


import sys
import re
import itertools
from cgi import escape as escape_html

if sys.version_info < (2, 6):
    #import shedskin_itertools as itertools  # not used izip_longest() returns incorrect results
    
    def izip_longest(*args, **kwds):
        """Taken from py 2.6 docs
        izip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-
        """
        fillvalue = kwds.get('fillvalue')
        
        def sentinel(counter=([fillvalue] * (len(args) - 1)).pop):
            yield counter()  # yields the fillvalue, or raises IndexError
        fillers = itertools.repeat(fillvalue)
        iters = [itertools.chain(it, sentinel(), fillers) for it in args]
        try:
            for tup in itertools.izip(*iters):
                yield tup
        except IndexError:
            pass
else:
    from itertools import izip_longest


import diff_match_patch  # from http://code.google.com/p/google-diff-match-patch/


def side_by_side_diff(old_text, new_text):
    """
    Calculates a side-by-side line-based difference view.
    
    Wraps insertions in <ins></ins> and deletions in <del></del>.
    """
    def yield_open_entry(open_entry):
        """ Yield all open changes. """
        ls, rs = open_entry
        # Get unchanged parts onto the right line
        if ls[0] == rs[0]:
            yield (False, ls[0], rs[0])
            #for l, r in itertools.izip_longest(ls[1:], rs[1:]):
            for tmp_x in izip_longest(ls[1:], rs[1:]):
                l, r = tmp_x
                yield (True, l, r)
        elif ls[-1] == rs[-1]:
            #for l, r in itertools.izip_longest(ls[:-1], rs[:-1]):
            for tmp_x in izip_longest(ls[:-1], rs[:-1]):
                l, r = tmp_x
                yield (l != r, l, r)
            yield (False, ls[-1], rs[-1])
        else:
            #for l, r in itertools.izip_longest(ls, rs):
            for tmp_x in izip_longest(ls, rs):
                l, r = tmp_x
                yield (True, l, r)
 
    line_split = re.compile(r'(?:\r?\n)')
    dmp = diff_match_patch.diff_match_patch()

    diff = dmp.diff_main(old_text, new_text)
    dmp.diff_cleanupSemantic(diff)

    open_entry = ([None], [None])
    for change_type, entry in diff:
        assert change_type in [-1, 0, 1]

        entry = (entry.replace('&', '&amp;')
                      .replace('<', '&lt;')
                      .replace('>', '&gt;'))

        lines = line_split.split(entry)

        # Merge with previous entry if still open
        ls, rs = open_entry

        line = lines[0]
        if line:
            if change_type == 0:
                ls[-1] = ls[-1] or ''
                rs[-1] = rs[-1] or ''
                ls[-1] = ls[-1] + line
                rs[-1] = rs[-1] + line
            elif change_type == 1:
                rs[-1] = rs[-1] or ''
                #rs[-1] += '<ins>%s</ins>' % line if line else ''  #requires 2.5+
                # works with py 2.4 START
                if line:
                    tmp_line = line
                else:
                    tmp_line = ''
                rs[-1] += '<ins>%s</ins>' % tmp_line
                del(tmp_line)
                # works with py 2.4 END
            elif change_type == -1:
                ls[-1] = ls[-1] or ''
                #ls[-1] += '<del>%s</del>' % line if line else ''  #requires 2.5+
                # works with py 2.4 START
                if line:
                    tmp_line = line
                else:
                    tmp_line = ''
                ls[-1] += '<del>%s</del>' % tmp_line
                del(tmp_line)
                # works with py 2.4 END
                
        lines = lines[1:]

        if lines:
            if change_type == 0:
                # Push out open entry
                for entry in yield_open_entry(open_entry):
                    yield entry
                
                # Directly push out lines until last
                for line in lines[:-1]:
                    yield (False, line, line)
                
                # Keep last line open
                open_entry = ([lines[-1]], [lines[-1]])
            elif change_type == 1:
                ls, rs = open_entry
                
                for line in lines:
                    #rs.append('<ins>%s</ins>' % line if line else '')
                    if line:
                        tmp_line = line
                    else:
                        tmp_line = ''
                    rs.append('<ins>%s</ins>' % tmp_line)
                    del(tmp_line)
                
                open_entry = (ls, rs)
            elif change_type == -1:
                ls, rs = open_entry
                
                for line in lines:
                    #ls.append('<del>%s</del>' % line if line else '')
                    if line:
                        tmp_line = line
                    else:
                        tmp_line = ''
                    ls.append('<del>%s</del>' % tmp_line)
                    del(tmp_line)
                
                open_entry = (ls, rs)

    # Push out open entry
    for entry in yield_open_entry(open_entry):
        yield entry


GEN_HTML_SIDE_BY_SIDE_DIFF_CSS = """td {
    border: solid 1px;
}

.nochange {
    background: #eee;
}

.change .diff-content.left {
    background-color: #ffa;
}

.change .diff-content.right {
    background-color: #afa;
}


.diff-content del {
    color: red;
    text-decoration: underline;
}

.diff-content ins {
    color: #0a0;
    text-decoration: underline;
}

code {
    white-space: pre-wrap; /* css-3 */
    white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */
    white-space: -pre-wrap; /* Opera 4-6 */
    white-space: -o-pre-wrap; /* Opera 7 */
    word-wrap: break-word; /* Internet Explorer 5.5+ */
}
"""


def gen_html_side_by_side_diff(old_text, new_text, old_name=None, new_name=None):
    """Returns a string containing a html table def
    """
    
    old_name = old_name or 'Old'
    new_name = new_name or 'New'
    if isinstance(old_text, unicode) and isinstance(new_text, unicode):
        treat_as_unicode = True
    else:
        treat_as_unicode = False
    
    def process_text_entry(in_str):
        if not in_str:
            in_str = '&nbsp;'
        if treat_as_unicode:
            in_str = in_str.encode('us-ascii', 'xmlcharrefreplace')
        #in_str = in_str.replace(' ', '&nbsp;')
        #return in_str
        return '<code>' + in_str + '</code>'  # indentation is missing (unless use css above for code) :-( but vertical size if good
        #return '<pre>' + in_str + '</pre>'  # indentation correct, but lines are too high
        #return '<pre><code>' + in_str + '</code></pre>'  # indentation correct, but lines are too high
    
    result = []
    result.append('''<table width="100%"> <tbody>''')
    result.append('''<tr><th></th><th width="50%%">%s</th><th></th><th width="50%%">%s</th></tr>''' % (old_name, new_name))  # fixme, unicode names need escaping
    diff_counter = 0
    for diff_type, diff_lhs, diff_rhs in side_by_side_diff(old_text, new_text):
        if diff_type:
            change_class = 'change'
            change_type_left = '-'
            change_type_right = '+'
            diff_counter += 1
            line_id = ' id="diffline_%d"' % diff_counter
        else:
            change_class = 'nochange'
            change_type_right = change_type_left = ' '
            line_id = ''
        change_status_class_right = change_status_class_left = 'change-status'
        
        if diff_lhs is None:
            change_status_class_left = 'left'
            change_type_left = ' '
            
        if diff_rhs is None:
            change_status_class_right = 'right'
            change_type_right = ' '
        
        result.append('''<tr class="%s"%s><td class="%s">%s</td><td class="diff-content left">%s</td><td class="%s">%s</td><td class="diff-content right">%s</td> </tr>''' % (change_class, line_id, change_status_class_left, change_type_left, process_text_entry(diff_lhs), change_status_class_right, change_type_right, process_text_entry(diff_rhs)))
    result.append('''</tbody> </table> ''')
    for x in range(1, diff_counter + 1):
        tmp_html = ''' <a href="javascript:void(0)" onClick="goToByScroll('diffline_%d')">%d</a>''' % (x,x)
        result.insert(x - 1, tmp_html)
    return '\n'.join(result)


def simple_html_diff(old_text, new_text, title=None, old_name=None, new_name=None):
    """includes javascript (well requests it from google)
    for jumping to next diff with "n" key and previous with 'p'
    (shift + key does the reverse).
    """
    title = title or 'Test diff'
    result = []
    result.append('''<html>
  <head>
    <title>%s</title>
    
<style type="text/css">
''' % escape_html(title))
    result.append(GEN_HTML_SIDE_BY_SIDE_DIFF_CSS)
    result.append('''
</style>

    <script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js'></script>
    <script src="http://api.jquery.com/scripts/events.js"></script>
    <script>
    function goToByScroll(id){
          $('html,body').animate({scrollTop: $("#"+id).offset().top},'fast');
    }
    </script>

</head>
<body>
Diffs:
''')  # defauls to fast speed, could be set to 'slow'
    result.append(gen_html_side_by_side_diff(old_text, new_text, old_name=old_name, new_name=new_name))
    result.append('''

<script>
var current_diff_num = 0;
$(document).keypress(function(event) {
  if ( event.which == 13 ) {  // enter key was pressed
     event.preventDefault();
   }
    if ( event.which == 110  || event.which == 80 ) {  // 'n' or 'P' key was pressed
        current_diff_num++;
        var jump_target = "diffline_" + current_diff_num;
        goToByScroll(jump_target);
    }
    if ( event.which == 78 || event.which == 112) {  // 'N' or 'p' key was pressed
        current_diff_num--;
        var jump_target = "diffline_" + current_diff_num;
        goToByScroll(jump_target);
    }
});

$("#other").click(function() {
  $(document).keypress();
});</script>

</body>
</html>
''')
    return '\n'.join(result)

if __name__ == '__main__':
    # The '\xf6' is the same whether string is Unicode type or byte type encoded in latin1

    text1 = u"""This is line 1.
and this is line 2

This is really line 4, the 3rd one was blank.
This will be removed.
This too :-(

1 2 3 4 5
Tada!
1 2 3 4 5 6
    This is supposed to be indented.
Bj\xf6rk
1 2   4 5 6
#1 son
This is a really, really, really, really, really, really, really, really, really long line. Like supper long big duper long and then some.
last line 0 < 1 > 2
"""

    text2 = u"""This is line number one!
and this is line 2
Line 3
This is really line 4, the third one was blank.

1 2 3 4 5 6
Tada!
1 2 3 4 5
    This is supposed to be indented.
Bj\xf6rk
  2 3 4 V 6 7
This is a new line
And so is this
and this one too!
#1 son
last line 0 < 1 > 2
"""

    """
    for x in side_by_side_diff(text1, text2):
        print x

    print '-'*65
    """
    
    
    print simple_html_diff(text1, text2)
    raise SystemExit
    print '''<html>
  <head>
    <title>Test diff</title>
    
<style type="text/css">
'''
    print GEN_HTML_SIDE_BY_SIDE_DIFF_CSS
    print '''
</style>
</head>
<body>
'''
    print gen_html_side_by_side_diff(text1, text2)
    print '''</body>
</html>
'''
